﻿namespace WebServiceExtensions.LogExtension
{
	#region Namespaces

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Web.Services.Protocols;
    using System.Xml;

    using WebServiceExtensions.CallCounterExtension;
    using WebServiceExtensions.Common;
    using WebServiceExtensions.Configurations.Common;
    using WebServiceExtensions.Configurations.LogExtension;

    using TraceSource = System.Diagnostics.TraceSource;

    #endregion Namespaces

	/// <summary>
	///   The soap message log extension.
	/// </summary>
	public class SoapMessageLogExtension : SoapExtension
	{
		#region Fields (5) 

        /// <summary>
        /// Gets the call counter manager.
        /// </summary>
        private static CallCounter callCounterManager;

        /// <summary>
        /// Represents config view
        /// </summary>
        private WebServiceExtensionConfigurationView configView = new WebServiceExtensionConfigurationView();

		/// <summary>
		///   The soap logger.
		/// </summary>
		private ISoapLogger soapLogger;

		/// <summary>
		///   The soap log stream.
		/// </summary>
		private SoapMessageLogExtensionStream soapLogStream;

        /// <summary>
        /// The trace source
        /// </summary>
	    private TraceSource traceSource;

		#endregion Fields 

		#region Properties (2) 

        /// <summary>
        /// Gets the call counter manager.
        /// </summary>
        /// <value>The call counter manager.</value>
        public static CallCounter CallCounterManager
        {
            get
            {
                if (callCounterManager == null)
                {
                    callCounterManager = new CallCounter();
                    callCounterManager.InitializeCounters();
                }

                return callCounterManager;
            }

            private set
            {
                callCounterManager = value;
            }
        }

        /// <summary>
        /// Gets or sets the config view.
        /// </summary>
        /// <value>The config view.</value>
	    internal WebServiceExtensionConfigurationView ConfigView
	    {
	        get
	        {
	            return this.configView;
	        }

	        set
	        {
	            this.configView = value;
	        }
	    }

		#endregion Properties 

		#region Methods (11) 

		// Public Methods (5) 

		/// <summary>
		///   When overridden in a derived class, allows a SOAP extension access to the memory buffer containing the SOAP request or response.
		/// </summary>
		/// <param name = "stream">
		///   A memory buffer containing the SOAP request or response.
		/// </param>
		/// <returns>
		///   A <see cref = "T:System.IO.Stream" /> representing a new memory buffer that this SOAP extension can modify.
		/// </returns>
		public override Stream ChainStream(Stream stream)
		{
			this.soapLogStream = new SoapMessageLogExtensionStream(stream);
			return this.soapLogStream;
		}

		/// <summary>
		///   Gets the initializer.
		/// </summary>
		/// <param name = "webServiceType">
		///   Type of the web service.
		/// </param>
		/// <returns>
		///   The get initializer.
		/// </returns>
		public override object GetInitializer(Type webServiceType)
		{
			if (WebServiceExtensionConfigurationView.Exists())
			{
                switch (this.configView.SoapMessageLogger.LoggerType)
                {
                    case LoggerType.Custom:
                        if (this.configView.SoapMessageLogger.Custom == null)
                        {
                            throw new ApplicationException("Custom type information is missing.");
                        }

                        return Activator.CreateInstance(this.configView.SoapMessageLogger.Custom.Type);
                    case LoggerType.TraceSource:
                        return new TraceSource(this.configView.SoapMessageLogger.TraceSource.Name);
                }
			}

			return null;
		}

		/// <summary>
		///   When overridden in a derived class, allows a SOAP extension to initialize data specific to an XML Web service method using an attribute applied to the XML Web service method at a one time performance cost.
		/// </summary>
		/// <param name = "methodInfo">
		///   A <see cref = "T:System.Web.Services.Protocols.LogicalMethodInfo" /> representing the specific function prototype for the XML Web service method to which the SOAP extension is applied.
		/// </param>
		/// <param name = "attribute">
		///   The <see cref = "T:System.Web.Services.Protocols.SoapExtensionAttribute" /> applied to the XML Web service method.
		/// </param>
		/// <returns>
		///   The <see cref = "T:System.Object" /> that the SOAP extension initializes for caching.
		/// </returns>
		public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
		{
			if (WebServiceExtensionConfigurationView.Exists())
			{
			    switch (this.configView.SoapMessageLogger.LoggerType)
			    {
			        case LoggerType.Custom:
			            if (this.configView.SoapMessageLogger.Custom == null)
			            {
			                throw new ApplicationException("Custom type information is missing.");
			            }

			            return Activator.CreateInstance(this.configView.SoapMessageLogger.Custom.Type);
			        case LoggerType.TraceSource:
			            return new TraceSource(this.configView.SoapMessageLogger.TraceSource.Name);
			    }
			}

		    return null;
		}

		/// <summary>
		///   When overridden in a derived class, allows a SOAP extension to initialize itself using the data cached in the <see
		///    cref = "M:System.Web.Services.Protocols.SoapExtension.GetInitializer(System.Web.Services.Protocols.LogicalMethodInfo,System.Web.Services.Protocols.SoapExtensionAttribute)" /> method.
		/// </summary>
		/// <param name = "initializer">
		///   The <see cref = "T:System.Object" /> returned from <see
		///    cref = "M:System.Web.Services.Protocols.SoapExtension.GetInitializer(System.Web.Services.Protocols.LogicalMethodInfo,System.Web.Services.Protocols.SoapExtensionAttribute)" /> cached by ASP.NET.
		/// </param>
		public override void Initialize(object initializer)
		{
			this.soapLogger = initializer as ISoapLogger;

            if (this.soapLogger == null)
            {
                this.traceSource = initializer as TraceSource;
                if (this.traceSource == null)
                {
                    throw new ApplicationException("LoggerType setting is missing");
                }
            }
		}

		/// <summary>
		///   When overridden in a derived class, allows a SOAP extension to receive a <see
		///    cref = "T:System.Web.Services.Protocols.SoapMessage" /> to process at each <see
		///    cref = "T:System.Web.Services.Protocols.SoapMessageStage" />.9
		/// </summary>
		/// <param name = "message">
		///   The <see cref = "T:System.Web.Services.Protocols.SoapMessage" /> to process.
		/// </param>
		public override void ProcessMessage(SoapMessage message)
		{
			SoapMessageLogType logType = SoapMessageLogType.None;
    		bool getConfigSetting = false;

			ConfigSettings settings = null;

			switch (message.Stage)
			{
				case SoapMessageStage.BeforeSerialize:
					{
						getConfigSetting = true;
						logType = SoapMessageLogType.Request;
						break;
					}

				case SoapMessageStage.AfterSerialize:
					{
						getConfigSetting = true;
						logType = SoapMessageLogType.Request;
						break;
					}

				case SoapMessageStage.BeforeDeserialize:
					{
						getConfigSetting = true;
						logType = SoapMessageLogType.Response;
						break;
					}
			}

			if (getConfigSetting)
			{
				settings = GetConfigurationDetailsForRequst(logType, message, this.configView);
			}

			if (settings == null || !settings.LoggingEnabled)
			{
				return;
			}

			if (this.soapLogger == null && this.traceSource == null)
			{
				return;
			}

			switch (message.Stage)
			{
				case SoapMessageStage.BeforeSerialize:
					this.soapLogStream.SwitchToNewStream();
					break;
				case SoapMessageStage.AfterSerialize:
					// If message was written to memory buffer, write its content to log and copy to the SOAP stream
					if (this.soapLogStream.IsNewStream)
					{
						this.soapLogStream.Position = 0;
						this.InvokeMethodForLogging(SoapMessageLogType.Request, message, settings);
						this.soapLogStream.Position = 0;
						this.soapLogStream.CopyNewToOld();
					}

					break;
				case SoapMessageStage.BeforeDeserialize:
					this.soapLogStream.SwitchToNewStream();
					this.soapLogStream.CopyOldToNew();
                    this.InvokeMethodForLogging(SoapMessageLogType.Response, message, settings);
					this.soapLogStream.Position = 0;
					break;
			}
		}

		// Private Methods (6) 

		/// <summary>
		///   Determines whether [is logging enabled] [the specified message log type].
		/// </summary>
		/// <param name = "messageLogType">Type of the message log.</param>
		/// <param name = "message">The message.</param>
		/// <param name = "configView">The config view.</param>
		/// <returns>
		///   <c>true</c> if [is logging enabled] [the specified message log type]; otherwise, <c>false</c>.
		/// </returns>
		private static ConfigSettings GetConfigurationDetailsForRequst(
			SoapMessageLogType messageLogType, SoapMessage message, WebServiceExtensionConfigurationView configView)
		{
			ConfigSettings setting = new ConfigSettings();
			string typeName = message.MethodInfo.DeclaringType.FullName;
			string methodName = message.MethodInfo.Name;
			Uri serviceUri = new Uri(message.Url);
			string host = serviceUri.Host;

			if (configView.SoapMessageLogger == null || string.IsNullOrWhiteSpace(configView.SoapMessageLogger.Name))
			{
				setting.LoggingEnabled = false;
				return setting;
			}

			if (configView.SoapMessageLogger.SoapMessageLogSettings != null)
			{
				GetSettingsFromSoapMessageLogSettings(messageLogType, configView.SoapMessageLogger.SoapMessageLogSettings, setting);
				if (setting.LoggingEnabled)
				{
					if (configView.SoapMessageLogger.SoapMessageLogSettings.Types != null
						&& configView.SoapMessageLogger.SoapMessageLogSettings.Types.Count > 0)
					{
						TypeData type = configView.SoapMessageLogger.SoapMessageLogSettings.Types[typeName];
						if (type != null)
						{
							GetSettingsFromType(messageLogType, host, type, setting);
							if (setting.LoggingEnabled)
							{
								if (type.Methods != null && type.Methods.Count > 0)
								{
									Method method = type.Methods[methodName];
									if (method == null)
									{
										return setting;
									}

									GetSettingsFromMethod(messageLogType, method, setting);
								}
							}
						}
					}
				}
			}

			return setting;
		}

		/// <summary>
		///   Gets the settings from method.
		/// </summary>
		/// <param name = "messageLogType">Type of the message log.</param>
		/// <param name = "method">The method.</param>
		/// <param name = "setting">The setting.</param>
		private static void GetSettingsFromMethod(SoapMessageLogType messageLogType, Method method, ConfigSettings setting)
		{
			if (!setting.LoggingEnabled)
			{
				return;
			}

			setting.EnableCallCounting = setting.EnableCallCounting ? method.EnableCallCounting : setting.EnableCallCounting;
			setting.LogCallCount = setting.LogCallCount ? method.LogCallCount : setting.LogCallCount;
			setting.LogCallType = setting.LogCallType ? method.LogCallType : setting.LogCallType;

			if (messageLogType == SoapMessageLogType.Request && !method.LogSoapRequest)
			{
				setting.LoggingEnabled = false;
			}

			if (messageLogType == SoapMessageLogType.Response && !method.LogSoapResponse)
			{
				setting.LoggingEnabled = false;
			}

			setting.LogServiceUrl = setting.LogServiceUrl ? method.LogServiceUrl : setting.LogServiceUrl;
			setting.LogSoapHeaders = setting.LogSoapHeaders ? method.LogSoapHeaders : setting.LogSoapHeaders;
			setting.LogSoapRequest = setting.LogSoapRequest ? method.LogSoapRequest : setting.LogSoapRequest;
			setting.LogSoapResponse = setting.LogSoapResponse ? method.LogSoapResponse : setting.LogSoapResponse;
			setting.LogSoapVersion = setting.LogSoapVersion ? method.LogSoapVersion : setting.LogSoapVersion;
		}

		/// <summary>
		///   Gets the setting from SOAP message log settings.
		/// </summary>
		/// <param name = "logType">Type of the log.</param>
		/// <param name = "soapMessageLogSettings">The SOAP message log settings.</param>
		/// <param name = "setting">The setting.</param>
		private static void GetSettingsFromSoapMessageLogSettings(
			SoapMessageLogType logType, SoapMessageLogSettings soapMessageLogSettings, ConfigSettings setting)
		{
			setting.EnableCallCounting = soapMessageLogSettings.EnableCallCounting;
			setting.LogCallCount = soapMessageLogSettings.LogCallCount;
			setting.LogCallType = soapMessageLogSettings.LogCallType;
			if (logType == SoapMessageLogType.Request && !soapMessageLogSettings.LogSoapRequest)
			{
				setting.LoggingEnabled = false;
			}

			if (logType == SoapMessageLogType.Response && !soapMessageLogSettings.LogSoapResponse)
			{
				setting.LoggingEnabled = false;
			}

			setting.LogServiceUrl = soapMessageLogSettings.LogServiceUrl;
			setting.LogSoapHeaders = soapMessageLogSettings.LogSoapHeaders;
			setting.LogSoapRequest = soapMessageLogSettings.LogSoapRequest;
			setting.LogSoapResponse = soapMessageLogSettings.LogSoapResponse;
			setting.LogSoapVersion = soapMessageLogSettings.LogSoapVersion;
		}

		/// <summary>
		///   Gets the type of the setting from.
		/// </summary>
		/// <param name = "logType">Type of the log.</param>
		/// <param name = "host">The host.</param>
		/// <param name = "type">The type.</param>
		/// <param name = "setting">The setting.</param>
		private static void GetSettingsFromType(
			SoapMessageLogType logType, string host, TypeData type, ConfigSettings setting)
		{
			if (!setting.LoggingEnabled)
			{
				return;
			}

			setting.EnableCallCounting = setting.EnableCallCounting ? type.EnableCallCounting : setting.EnableCallCounting;
			setting.LogCallCount = setting.LogCallCount ? type.LogCallCount : setting.LogCallCount;
			setting.LogCallType = setting.LogCallType ? type.LogCallType : setting.LogCallType;

			if (logType == SoapMessageLogType.Request && !type.LogSoapRequest)
			{
				setting.LoggingEnabled = false;
			}

			if (logType == SoapMessageLogType.Response && !type.LogSoapResponse)
			{
				setting.LoggingEnabled = false;
			}

			if (setting.LoggingEnabled)
			{
				if (!string.IsNullOrWhiteSpace(type.Hosts))
				{
					string[] hostList = type.Hosts.Split(new[] { Constants.CommaSeperator }, StringSplitOptions.RemoveEmptyEntries);
					string found = Array.Find(hostList, str => str.ToLowerInvariant().Equals(host.ToLowerInvariant()));
					if (string.IsNullOrWhiteSpace(found))
					{
						setting.LoggingEnabled = false;
					}
				}
			}

			setting.LogServiceUrl = setting.LogServiceUrl ? type.LogServiceUrl : setting.LogServiceUrl;
			setting.LogSoapHeaders = setting.LogSoapHeaders ? type.LogSoapHeaders : setting.LogSoapHeaders;
			setting.LogSoapRequest = setting.LogSoapRequest ? type.LogSoapRequest : setting.LogSoapRequest;
			setting.LogSoapResponse = setting.LogSoapResponse ? type.LogSoapResponse : setting.LogSoapResponse;
			setting.LogSoapVersion = setting.LogSoapVersion ? type.LogSoapVersion : setting.LogSoapVersion;
		}

		/// <summary>
		///   Invokes the method for logging.
		/// </summary>
		/// <param name = "messageLogType">Type of the message log.</param>
		/// <param name = "message">The message.</param>
		/// <param name = "settings">The settings.</param>
		private void InvokeMethodForLogging(
			SoapMessageLogType messageLogType, SoapMessage message, ConfigSettings settings)
		{
            if (messageLogType == SoapMessageLogType.Request)
            {
                if (settings.EnableCallCounting)
                {
                    if (CallCounterManager == null)
                    {
                        CallCounterManager = new CallCounter();
                        CallCounterManager.InitializeCounters();
                    }

                    string typeName = message.MethodInfo.DeclaringType.FullName;
                    string methodName = message.MethodInfo.Name;
                    CallCounterManager.UpdateCount(typeName, methodName);
                }
            }

			SoapMessageDetails messageDetails = this.PopulateSoapMessageDetails(messageLogType, message, settings);
            if (this.soapLogger != null)
            {
                if (this.configView.SoapMessageLogger.Custom.OutputType == OutputType.SoapMessageDetailsClass)
                {
                    this.soapLogger.GetSoapMessageDetailsForLogging(messageLogType, messageDetails);
                }
                else
                {
                    Serializer<SoapMessageDetails> serializer = new Serializer<SoapMessageDetails>(SerializerType.XmlSerializer);
                    string str = serializer.Serialize(messageDetails);
                    this.soapLogger.GetSoapMessageDetailsForLogging(messageLogType, str);
                }
            }
            else
            {
                Serializer<SoapMessageDetails> serializer = new Serializer<SoapMessageDetails>(SerializerType.XmlSerializer);
                string str = serializer.Serialize(messageDetails);
                
                this.traceSource.TraceData(TraceEventType.Information, 1, str);
            }
		}

		/// <summary>
		///   Populates the SOAP message details.
		/// </summary>
		/// <param name = "messageLogType">Type of the message log.</param>
		/// <param name = "message">The message.</param>
		/// <param name = "settings">The settings.</param>
        /// <returns>The SoapMessageDetails </returns>
		private SoapMessageDetails PopulateSoapMessageDetails(
			SoapMessageLogType messageLogType, SoapMessage message, ConfigSettings settings)
		{
			SoapMessageDetails soapMessageDetails = new SoapMessageDetails();
			TextReader reader = new StreamReader(this.soapLogStream);
			string soapString = reader.ReadToEnd();
			soapMessageDetails.RequestType = messageLogType;

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(soapString);

			if (messageLogType == SoapMessageLogType.Request && settings.LogSoapRequest)
			{
			    soapMessageDetails.SoapEnvelop = doc;
			}
			else if (messageLogType == SoapMessageLogType.Response && settings.LogSoapResponse)
			{
			    soapMessageDetails.SoapEnvelop = doc;
			}
			else
			{
			    soapMessageDetails.SoapEnvelop = null;
			}

			soapMessageDetails.SoapVersion = settings.LogSoapVersion ? message.SoapVersion.ToString() : null;

			soapMessageDetails.SoapHeaders = settings.LogSoapHeaders ? message.Headers : null;

			soapMessageDetails.ServiceUrl = settings.LogServiceUrl ? message.Url : null;

			if (settings.LogCallType)
			{
				soapMessageDetails.CallType = message.OneWay ? CallType.Async : CallType.Sync;
			}
			else
			{
			    soapMessageDetails.CallType = null;
			}

            if (settings.LogCallCount)
            {
                string typeName = message.MethodInfo.DeclaringType.FullName;
                string methodName = message.MethodInfo.Name;
                soapMessageDetails.CallCount = CallCounterManager.GetCountForMethod(typeName, methodName);
            }
            else
            {
                soapMessageDetails.CallCount = null;
            }

			return soapMessageDetails;
		}

		#endregion Methods 
	}
}